var isArray = Array.isArray;
var root = this;

var defaultMaxListeners = 10
export default class EventEmitter {
  constructor(props) {
    super(props)
  }

  // By default EventEmitters will print a warning if more than
  // 10 listeners are added to it. This is a useful default which
  // helps finding memory leaks.
  //
  // Obviously not all Emitters should be limited to 10. This function allows
  // that to be increased. Set to zero for unlimited.
  setMaxListeners(n) {
    if (!this._events) this._events = {};
    this._maxListeners = n;
  }


  emit() {
    var type = arguments[0];
    // If there is no 'error' event listener then throw.
    if (type === 'error') {
      if (!this._events || !this._events.error ||
        (isArray(this._events.error) && !this._events.error.length)) {
        if (this.domain) {
          var er = arguments[1];
          er.domain_emitter = this;
          er.domain = this.domain;
          er.domain_thrown = false;
          this.domain.emit('error', er);
          return false;
        }

        if (arguments[1] instanceof Error) {
          throw arguments[1]; // Unhandled 'error' event
        } else {
          throw new Error("Uncaught, unspecified 'error' event.");
        }
        return false;
      }
    }

    if (!this._events) return false;
    var handler = this._events[type];
    if (!handler) return false;

    if (typeof handler == 'function') {
      if (this.domain) {
        this.domain.enter();
      }
      switch (arguments.length) {
        // fast cases
        case 1:
          handler.call(this);
          break;
        case 2:
          handler.call(this, arguments[1]);
          break;
        case 3:
          handler.call(this, arguments[1], arguments[2]);
          break;
          // slower
        default:
          var l = arguments.length;
          var args = new Array(l - 1);
          for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
          handler.apply(this, args);
      }
      if (this.domain) {
        this.domain.exit();
      }
      return true;

    } else if (isArray(handler)) {
      if (this.domain) {
        this.domain.enter();
      }
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];

      var listeners = handler.slice();
      for (var i = 0, l = listeners.length; i < l; i++) {
        listeners[i].apply(this, args);
      }
      if (this.domain) {
        this.domain.exit();
      }
      return true;

    } else {
      return false;
    }
  };

  addListener(type, listener) {
    if ('function' !== typeof listener) {
      throw new Error('addListener only takes instances of Function');
    }

    if (!this._events) this._events = {};

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, typeof listener.listener === 'function' ?
      listener.listener : listener);

    if (!this._events[type]) {
      // Optimize the case of one listener. Don't need the extra array object.
      this._events[type] = listener;
    } else if (isArray(this._events[type])) {

      // If we've already got an array, just append.
      this._events[type].push(listener);

    } else {
      // Adding the second element, need to change to array.
      this._events[type] = [this._events[type], listener];

    }

    // Check for listener leak
    if (isArray(this._events[type]) && !this._events[type].warned) {
      var m;
      if (this._maxListeners !== undefined) {
        m = this._maxListeners;
      } else {
        m = defaultMaxListeners;
      }

      if (m && m > 0 && this._events[type].length > m) {
        this._events[type].warned = true;
        console.error('(node) warning: possible EventEmitter memory ' +
          'leak detected. %d listeners added. ' +
          'Use emitter.setMaxListeners() to increase limit.',
          this._events[type].length);
        console.trace();
      }
    }

    return this;
  };

  on() {
    return this.addListener(...arguments);
  }

  once(type, listener) {
    if ('function' !== typeof listener) {
      throw new Error('.once only takes instances of Function');
    }

    var self = this;

    function g() {
      self.removeListener(type, g);
      listener.apply(this, arguments);
    };

    g.listener = listener;
    self.on(type, g);

    return this;
  };

  removeListener(type, listener) {
    if ('function' !== typeof listener) {
      throw new Error('removeListener only takes instances of Function');
    }

    // does not use listeners(), so no side effect of creating _events[type]
    if (!this._events || !this._events[type]) return this;

    var list = this._events[type];

    if (isArray(list)) {
      var position = -1;
      for (var i = 0, length = list.length; i < length; i++) {
        if (list[i] === listener ||
          (list[i].listener && list[i].listener === listener)) {
          position = i;
          break;
        }
      }

      if (position < 0) return this;
      list.splice(position, 1);
    } else if (list === listener ||
      (list.listener && list.listener === listener)) {
      delete this._events[type];
    }

    return this;
  };

  removeAllListeners(type) {
    if (arguments.length === 0) {
      this._events = {};
      return this;
    }

    var events = this._events && this._events[type];
    if (!events) return this;

    if (isArray(events)) {
      events.splice(0);
    } else {
      this._events[type] = null;
    }

    return this;
  };

  listeners(type) {
    if (!this._events) this._events = {};
    if (!this._events[type]) this._events[type] = [];
    if (!isArray(this._events[type])) {
      this._events[type] = [this._events[type]];
    }

  }
}
var Protocol = exports;

var HEADER = 5;

var Message = function(id, route, body) {
  this.id = id;
  this.route = route;
  this.body = body;
};

/**
 *
 *pomele client encode
 * id message id;
 * route message route
 * msg message body
 * socketio current support string
 *
 */
Protocol.encode = function(id, route, msg) {
  var msgStr = JSON.stringify(msg);
  if (route.length > 255) { throw new Error('route maxlength is overflow'); }
  var byteArray = new Uint16Array(HEADER + route.length + msgStr.length);
  var index = 0;
  byteArray[index++] = (id >> 24) & 0xFF;
  byteArray[index++] = (id >> 16) & 0xFF;
  byteArray[index++] = (id >> 8) & 0xFF;
  byteArray[index++] = id & 0xFF;
  byteArray[index++] = route.length & 0xFF;
  for (var i = 0; i < route.length; i++) {
    byteArray[index++] = route.charCodeAt(i);
  }
  for (var i = 0; i < msgStr.length; i++) {
    byteArray[index++] = msgStr.charCodeAt(i);
  }
  return bt2Str(byteArray, 0, byteArray.length);
};




/**
 *
 *client decode
 *msg String data
 *return Message Object
 */
Protocol.decode = function(msg) {
  var idx, len = msg.length,
    arr = new Array(len);
  for (idx = 0; idx < len; ++idx) {
    arr[idx] = msg.charCodeAt(idx);
  }
  var index = 0;
  var buf = new Uint16Array(arr);
  var id = ((buf[index++] << 24) | (buf[index++]) << 16 | (buf[index++]) << 8 | buf[index++]) >>> 0;
  var routeLen = buf[HEADER - 1];
  var route = bt2Str(buf, HEADER, routeLen + HEADER);
  var body = bt2Str(buf, routeLen + HEADER, buf.length);
  return new Message(id, route, body);
};

var bt2Str = function(byteArray, start, end) {
  var result = "";
  for (var i = start; i < byteArray.length && i < end; i++) {
    result = result + String.fromCharCode(byteArray[i]);
  };
  return result;
}

if (typeof Object.create !== 'function') {
  Object.create = function(o) {
    function F() {}
    F.prototype = o;
    return new F();
  };
}

var root = window;
var pomelo = Object.create(EventEmitter.prototype); // object extend from object
root.pomelo = pomelo;
var socket = null;
var id = 1;
var callbacks = {};

pomelo.init = function(params, cb) {
  pomelo.params = params;
  params.debug = true;
  var host = params.host;
  var port = params.port;

  var url = 'ws://' + host;
  if (port) {
    url += ':' + port;
  }

  socket = io(url, { 'force new connection': true, reconnect: false });

  socket.on('connect', function() {
    console.log('[pomeloclient.init] websocket connected!');
    if (cb) {
      cb(socket);
    }
  });

  socket.on('reconnect', function() {
    console.log('reconnect');
  });

  socket.on('message', function(data) {
    if (typeof data === 'string') {
      data = JSON.parse(data);
    }
    if (data instanceof Array) {
      processMessageBatch(pomelo, data);
    } else {
      processMessage(pomelo, data);
    }
  });

  socket.on('error', function(err) {
    console.log(err);
  });

  socket.on('disconnect', function(reason) {
    pomelo.emit('disconnect', reason);
  });
};

pomelo.disconnect = function() {
  if (socket) {
    socket.disconnect();
    socket = null;
  }
};

pomelo.request = function(route) {
  if (!route) {
    return;
  }
  var msg = {};
  var cb;
  arguments = Array.prototype.slice.apply(arguments);
  if (arguments.length === 2) {
    if (typeof arguments[1] === 'function') {
      cb = arguments[1];
    } else if (typeof arguments[1] === 'object') {
      msg = arguments[1];
    }
  } else if (arguments.length === 3) {
    msg = arguments[1];
    cb = arguments[2];
  }
  msg = filter(msg, route);
  id++;
  callbacks[id] = cb;
  var sg = Protocol.encode(id, route, msg);
  socket.send(sg);
};

pomelo.notify = function(route, msg) {
  this.request(route, msg);
};

var processMessage = function(pomelo, msg) {
  var route;
  if (msg.id) {
    //if have a id then find the callback function with the request
    var cb = callbacks[msg.id];

    delete callbacks[msg.id];
    if (typeof cb !== 'function') {
      console.log('[pomeloclient.processMessage] cb is not a function for request ' + msg.id);
      return;
    }

    cb(msg.body);
    return;
  }

  // server push message or old format message
  processCall(msg);

  //if no id then it should be a server push message
  function processCall(msg) {
    var route = msg.route;
    if (!!route) {
      if (!!msg.body) {
        var body = msg.body.body;
        if (!body) { body = msg.body; }
        pomelo.emit(route, body);
      } else {
        pomelo.emit(route, msg);
      }
    } else {
      pomelo.emit(msg.body.route, msg.body);
    }
  }
};

var processMessageBatch = function(pomelo, msgs) {
  for (var i = 0, l = msgs.length; i < l; i++) {
    processMessage(pomelo, msgs[i]);
  }
};

function filter(msg, route) {
  if (route.indexOf('area.') === 0) {
    msg.areaId = pomelo.areaId;
  }

  msg.timestamp = Date.now();
  return msg;
}
